package za.org.jcicada.comms.impl;

import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.comms.IConnection;
import za.org.jcicada.comms.IConnector;
import za.org.jcicada.comms.IMessageListener;
import za.org.jcicada.comms.exceptions.CommsException;
import za.org.jcicada.comms.messages.CommsMessage;

public abstract class ProxyBase<T> {

    /** Class logger */
    private static final Log logger = LogFactory.getLog(ProxyBase.class);

    /** Map of clients to connections. */
    protected Map<T, IConnection> clients = new HashMap<T, IConnection>();
    
    /** Connector for creating connections. */
    protected IConnector connector;

    /**
     * Create a ProxyBase.
     *
     * @param connector IConnector object for creating connections to the remote subject.
     */
    public ProxyBase(IConnector connector) {
        if (null == connector) {
            throw new IllegalArgumentException("connector may not be null");
        }
        this.connector = connector;
    }
    
    /**
     * Create a ProxyBase given a location URI
     * 
     * @param locationURI describing how to contact an acceptor
     */
    public ProxyBase(URI locationURI) {
        this(ConnectorFactory.createConnector(locationURI));
    }
    
    /**
     * Create a message listener in the derived class for the given client
     * @param client to create a listener for
     * @return a message listener object
     */
    protected abstract MessageListenerBase createReceiver(T client);

    /**
     * Send a message from the specified client. Implements lazy connection creation.
     * @param client the source from which the message is sent
     * @param msg the message object to send
     */
    protected void sendMessage(T client, CommsMessage msg) {
        IConnection connection;
        try {
            connection = getConnection(client);
        } catch (IOException ex) {
            logger.error(String.format("Failed to create connection for client %s", client), ex);
            return;
        }
        try {
            connection.sendMessage(msg);
        } catch (IOException ex) {
            logger.error(String.format("Failed to send message %s for client %s", msg, client), ex);
        }
    }

    /**
     * 
     * @param client
     */
    public final void informDisconnect(T client) {
        closeConnection(client);
    }

	/**
	 * Try to lookup a connection for the specified client else create one using
	 * the connector and record it for future reference.
	 * 
	 * @param client
	 *            the client object wish to send
	 * @return a connection object for the client
	 * @throws CommsException
	 */
	protected IConnection getConnection(T client) throws IOException {
		IConnection connection = clients.get(client);
		if (connection != null && connection.isClosed()) {
			clients.remove(client);
			connection = null;
		}
		if (connection == null) {
			connection = connector.connect();
			IMessageListener listener = createReceiver(client);
			connection.initialize(listener);
			clients.put(client, connection);
		}
		return connection;
	}

    /**
     * Close the connection for the specified client and remove if from the connection
     * pool.
     * @param client the client wishing to close its connection
     */
    protected void closeConnection(T client) {
        // TODO Do we need to create a connection to close it if it doesn't already exist?
        // TODO shouldn't we remove the connection from the map upon successful closing?
        if (clients.containsKey(client)) {
            IConnection connection;
            try {
                connection = getConnection(client);
            } catch (IOException ex) {
                logger.error(String.format("Failed to create connection for client %s while closing connection -- this should never be reached.", client), ex);
                return;
            }
            try {
                connection.close();
                clients.remove(client);
            } catch (IOException ex) {
                logger.error(String.format("Failed to close connection for client %s", client), ex);
            }
        }
    }

}
